QEMU-QTest && Libfuzzer源码分析(下) 0x01 TL;DR 续接上文,开始分析libfuzzer部分的代码。下文的前置知识基本在上文中已经覆盖。
0x02 generic-fuzz 文件在tests/qtest/fuzz/generic_fuzz.c中,入口函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static  void  register_generic_fuzz_targets (void )  {    fuzz_add_target(&(FuzzTarget){ 				     });     GString *name;     const  generic_fuzz_config *config; 		     for  (int  i = 0 ;          i < sizeof (predefined_configs) / sizeof (generic_fuzz_config);          i++) {         config = predefined_configs + i;         name = g_string_new("generic-fuzz" );         g_string_append_printf(name, "-%s" , config->name);         fuzz_add_target(&(FuzzTarget){ 						         });     } } fuzz_target_init(register_generic_fuzz_targets); 
 
作者没有设置的device就要自己去设置启动command,获取command的主要函数在generic_fuzz_cmdline中。
接来下看fuzz的前期准备generic_pre_fuzz函数(只截取部分重要代码):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 static  void  generic_pre_fuzz (QTestState *s)  {		     if  (!getenv("QEMU_FUZZ_OBJECTS" )) {         usage();     }     qts_global = s;      dma_regions = g_array_new(false , false , sizeof (address_range));     dma_patterns = g_array_new(false , false , sizeof (pattern));     fuzzable_memoryregions = g_hash_table_new(NULL , NULL );     fuzzable_pci_devices   = g_ptr_array_new();     result = g_strsplit(getenv("QEMU_FUZZ_OBJECTS" ), " " , -1 );     for  (int  i = 0 ; result[i] != NULL ; i++) {         printf ("Matching objects by name %s\n" , result[i]);         object_child_foreach_recursive(qdev_get_machine(),                                        locate_fuzz_objects,                                     result[i]);     } 		 } 
 
先看标感叹号那块代码,经调试,qdev-get-machine()这块得到的machine对象为pc-q35-6.0-machine,如下:
1 2 3 4 5 6 7 8 9 10 pwndbg> p/x *dev $1  = {   class = 0x614000005240 ,   free  = 0x7ffff6ccfba0 ,   properties = 0x61d000098f00 ,   ref = 0x2 ,   parent = 0x604000016cd0  } pwndbg> x/1 s dev.class.type.name 0x603000009b20 : "pc-q35-6.0-machine" 
 
继续看object_child_foreach_recursive这个函数,这个函数比较绕,调用层次也比较多,如果要细说估计都可以成一篇文章了,我就大概理一下思路:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 static  int  do_object_child_foreach (Object *obj,                                    int (*fn)(Object *child, void *opaque),                                    void  *opaque, bool  recurse) {     GHashTableIter iter;     ObjectProperty *prop;     int  ret = 0 ;     g_hash_table_iter_init(&iter, obj->properties);      while  (g_hash_table_iter_next(&iter, NULL , (gpointer *)&prop)) {         if  (object_property_is_child(prop)) {             Object *child = prop->opaque;                 ret = fn(child, opaque);                      if  (ret != 0 ) {                                   break ;             }             if  (recurse) {                          ret = do_object_child_foreach(child, fn, opaque, true );                 if  (ret != 0 ) {                     break ;                 }             }         }     }     return  ret; } 
 
为什么Object还有child Object?两者是什么关系?搞明白这个才能清楚这个函数的具体含义。
我举个例子,就拿前面我们得到的对象pc-q35-6.0-machine来举例,这个对象类似于一个根节点,定义在pc-q35.c代码文件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 DEFINE_Q35_MACHINE(v6_0, "pc-q35-6.0" , NULL ,                    pc_q35_6_0_machine_options); #define  DEFINE_Q35_MACHINE(suffix, name, compatfn, optionfn) \ 		static  void  pc_init_##suffix(MachineState *machine) \      { \         pc_q35_init(machine); \     } \     DEFINE_PC_MACHINE(suffix, name, pc_init_##suffix, optionfn) #define  DEFINE_PC_MACHINE(suffix, namestr, initfn, optsfn) \     static  const  TypeInfo pc_machine_type_##suffix = { \          .name       = namestr TYPE_MACHINE_SUFFIX, \         .parent     = TYPE_PC_MACHINE, \         .class_init = pc_machine_##suffix##_class_init, \     }; \     static  void  pc_machine_init_##suffix(void) \      { \         type_register(&pc_machine_type_##suffix); \     } \     type_init(pc_machine_init_##suffix)                    
 
再看看pc_q35_init这个函数:
1 2 3 4 5 6 7 8 static  void  pc_q35_init (MachineState *machine)  {    q35_host = Q35_HOST_DEVICE(qdev_new(TYPE_Q35_HOST_DEVICE));      object_property_add_child(qdev_get_machine(), "q35" , OBJECT(q35_host));       	 } 
 
标记1处的函数从名称可以看出是给object的property添加child,那么根据函数定义的参数名可以断定是给qdev_get_machine()所得到的object,也就是pc-q35-6.0-machine新增一个property,命名为q35,子对象为q35-pcihost。也就是在两个对象间新建一个链接关系,类似于链表,并对这个链表命名。简单来说创建的链条如下:
1 2 3 4 5 ObjectProperty *op; op->name = "q35" ; op->type = "child<q35-pcihost>" ; op->opaque = OBJECT(q35_host); 
 
当然了,这个“根”对象的child肯定不止这一个。通过对qdev_get_machine()的交叉引用可以发现mc146818rtc.c也是它的child:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static  void  rtc_realizefn (DeviceState *dev, Error **errp)  {  	   	ISADevice *isadev = ISA_DEVICE(dev);     RTCState *s = MC146818_RTC(dev);     object_property_add_tm(OBJECT(s), "date" , rtc_get_date);  		 } ISADevice *mc146818_rtc_init (ISABus *bus, int  base_year, qemu_irq intercept_irq)   {    ISADevice *isadev; 		     isadev = isa_new(TYPE_MC146818_RTC);     object_property_add_alias(qdev_get_machine(), "rtc-time" , OBJECT(isadev),                               "date" );      return  isadev; } 
 
object_property_add_tm函数创建的property的type类型为struct tm,object_property_add_alias函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ObjectProperty * object_property_add_alias(Object *obj, const  char  *name,                           Object *target_obj, const  char  *target_name) {     ObjectProperty *op;     ObjectProperty *target_prop;   	   	target_prop = object_property_find_err(target_obj, target_name,                                            &error_abort); 		prop_type = g_strdup(target_prop->type);     op = object_property_add(obj, name, prop_type,                              property_get_alias,                              property_set_alias,                              property_release_alias,                              prop); } 
 
object_property_add_alias所设置的type为target_obj中target_name的type,也就是ISADevice对象中名为data的type,为struct tm。结合起来看,总的来说创建的链条如下:
1 2 3 4 5 ObjectProperty *op; op->name = "rtc-time" ; op->type = "struct tm" ; op->opaque = OBJECT(isadev); 
 
下面就简单说一下链条,链条的type类型有多种,有child<*>、link<*>、string、bool、struct tm、uint8、uint16、uint32、uint64等等,分别对应着父子对象之间的关系类型。定义不同type的链条有不同的函数,如object_property_add_tm()对应struct tm的type、object_property_add_bool()对应bool的type,等等。但最终都会调用同一个函数object_property_try_add():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 ObjectProperty * object_property_try_add(Object *obj, const  char  *name, const  char  *type,                         ObjectPropertyAccessor *get,                         ObjectPropertyAccessor *set ,                         ObjectPropertyRelease *release,                         void  *opaque, Error **errp) {     ObjectProperty *prop;     size_t  name_len = strlen (name); 		     prop = g_malloc0(sizeof (*prop));     prop->name = g_strdup(name);      prop->type = g_strdup(type);      prop->get = get;     prop->set  = set ;     prop->release = release;     prop->opaque = opaque;     g_hash_table_insert(obj->properties, prop->name, prop);      return  prop; } 
 
这下object和child object就搞明白了,继续看do_object_child_foreach()函数,应该就很明朗了。
这个函数的作用就是,遍历根对象(pc-q35-6.0-machine)的子对象,筛选出type类型是child<*>的子对象,执行回调函数,并且递归遍历子对象继续执行相同的操作,一直循环反复,直到将所有子对象、子对象的子对象…都遍历完后就退出。 
根据调试情况可以验证我们的分析:
1 2 3 4 5 6 7 8 9 10 11 pwndbg> x/1 s prop.name 0x6020000440b0 : "rtc-time" pwndbg> x/1 s prop.type 0x6020000440d0 : "struct tm" pwndbg> x/1 s prop.name 0x60200002f8d0 : "q35" pwndbg> x/1 s prop.type 0x60300005f4a0 : "child<q35-pcihost>" 
 
回到generic_fuzz.c主函数中来,分析一下前面提到的回调函数locate_fuzz_objects():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 static  int  locate_fuzz_objects (Object *child, void  *opaque)  {    char  *pattern = opaque;                if  (g_pattern_match_simple(pattern, object_get_typename(child))) {                   object_child_foreach_recursive(child, locate_fuzz_memory_regions, NULL );                  if  (object_dynamic_cast(OBJECT(child), TYPE_PCI_DEVICE)) { 						             g_ptr_array_remove_fast(fuzzable_pci_devices, PCI_DEVICE(child));             g_ptr_array_add(fuzzable_pci_devices, PCI_DEVICE(child));         }     } else  if  (object_dynamic_cast(OBJECT(child), TYPE_MEMORY_REGION)) {          if  (g_pattern_match_simple(pattern,              object_get_canonical_path_component(child))) {             MemoryRegion *mr;             mr = MEMORY_REGION(child);              if  ((memory_region_is_ram(mr) ||                       memory_region_is_ram_device(mr) ||                  memory_region_is_rom(mr)) == false ) {                 g_hash_table_insert(fuzzable_memoryregions, mr, (gpointer)true );              }         }     }     return  0 ; } static  int  locate_fuzz_memory_regions (Object *child, void  *opaque)  {    const  char  *name;     MemoryRegion *mr;     if  (object_dynamic_cast(child, TYPE_MEMORY_REGION)) {          mr = MEMORY_REGION(child);          if  ((memory_region_is_ram(mr) ||              memory_region_is_ram_device(mr) ||             memory_region_is_rom(mr)) == false ) {             name = object_get_canonical_path_component(child);                          g_hash_table_insert(fuzzable_memoryregions, mr, (gpointer)true );          }     }     return  0 ; } 
 
先简要介绍一下两个函数,第一个是object_dynamic_cast(),该函数是用来判断两者object是否有父子关系(继承关系)的。由于这里用来判断是否和TYPE_MEMORY_REGION有继承关系,我查了一下QEMU目前没有继承自TYPE_MEMORY_REGION的情况,因此,这里这个函数的作用就是只判断传入的object是否为TYPE_MEMORY_REGION对象。 
第二个是object_get_canonical_path_component(),该函数是获取传入的object与父对象间的链接关系的名字,也就是链条ObjectProperty->name。
在locate_fuzz_objects函数中,当匹配到我们要fuzz的对象字符串时,又开始递归筛选child子对象,并执行locate_fuzz_memory_regions回调函数。该回调函数的作用是判断传入的child对象是否是TYPE_MEMORY_REGION对象,是就将memory空间保存。后续又判断该对象是否属于PCI继承的设备,属于则保存指针。
再往下看,如果匹配的不是我们要fuzz的对象字符串,而是TYPE_MEMORY_REGION对象,并且该对象与父对象的链条名称和我们输入的字符串相同的话,则保存memory空间。
回到最开始的地方来:
1 2 3 object_child_foreach_recursive(qdev_get_machine(),                                locate_fuzz_objects,                                result[i]); 
 
短短一行就展开了这么多的知识点,简单来概括一下前面所讲述的内容。
根据传入的QEMU_FUZZ_OBJECTS的值,假设传入了“virtio*”,那么该函数就是从“根”对象machine开始,不断的循环遍历子对象(继承对象),筛选出名称与“virtio*”相匹配的对象,或者是在一对继承关系中继承链条的名称与“virtio*”相匹配的子对象(且该子对象必须为TYPE_MEMORY_REGION),取出这些对象的MEMORY_REGION区域并保存,也相应的保存满足上述条件而且又属于PCI设备的对象。 
结合上手调试,理解的会更快一些。拿virtio-vga设备举例,我想要fuzz该设备,传入的QEMU_FUZZ_OBJECT的值为virtio*,那么当QEMU启动后,会匹配所有virtio*相关的对象或者memory region,其中,vga-pci.c文件中有这么一条:
1 2 memory_region_init_io(&subs[0 ], owner, &pci_vga_ioport_ops, s,                       "vga ioports remapped" , PCI_VGA_IOPORT_SIZE); 
 
也就是会与memory_region构建一条名为vga ioports remapped的关系链条。当然还有许多其他条相关的链,经调试得出的结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 * vga ioports remapped[0 ] (size 20 ) * qemu extended regs[0 ] (size 8 ) * virtio-pci-device[0 ] (size 1000 ) * vga[4 ] (size 1 ) * msix-pba[0 ] (size 8 ) * vga[2 ] (size 10 ) * bochs dispi interface[0 ] (size 16 ) * virtio-pci-notify[0 ] (size 1000 ) * vga[0 ] (size 2 ) * bus master[0 ] (size 0 ) * msix-table[0 ] (size 30 ) * vga-lowmem[0 ] (size 20000 ) * virtio-pci-common[0 ] (size 800 ) * virtio-pci-notify-pio[0 ] (size 4 ) * vbe[0 ] (size 4 ) * bus master container[0 ] (size 0 ) * virtio-vga-msix[0 ] (size 1000 ) * vga[3 ] (size 2 ) * virtio-pci-isr[0 ] (size 800 ) * virtio-pci[0 ] (size 4000 ) * vga[1 ] (size 1 ) 
 
总共会保存这些memory region。其中就包括了前面我们所说的vga ioports remapped。
继续往下看generic_pre_fuzz()函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 static  void  generic_pre_fuzz (QTestState *s)  {		     printf ("This process will try to fuzz the following MemoryRegions:\n" );     g_hash_table_iter_init(&iter, fuzzable_memoryregions);     while  (g_hash_table_iter_next(&iter, (gpointer)&mr, NULL )) {         printf ("  * %s (size %lx)\n" ,                object_get_canonical_path_component(&(mr->parent_obj)),                (uint64_t )mr->size);     }      if  (!g_hash_table_size(fuzzable_memoryregions)) {         printf ("No fuzzable memory regions found...\n" );         exit (1 );     }     pcibus = qpci_new_pc(s, NULL );      g_ptr_array_foreach(fuzzable_pci_devices, pci_enum, pcibus);      qpci_free_pc(pcibus);     counter_shm_init();  } 
 
这一块就是收尾工作,将PCI总线和保存的PCI设备初始化,总的来说就是做一些fuzz前的初始化准备工作。重点讲一下回调函数pci_enum():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static  void  pci_enum (gpointer pcidev, gpointer bus)  {    PCIDevice *dev = pcidev;     QPCIDevice *qdev;     int  i;     qdev = qpci_device_find(bus, dev->devfn);    																						     g_assert(qdev != NULL );     for  (i = 0 ; i < 6 ; i++) {         if  (dev->io_regions[i].size) {             qpci_iomap(qdev, i, NULL );         }     }     qpci_device_enable(qdev);     g_free(qdev); } 
 
dev->io_regions[]这个比较重要,是PCI Configuration space中的六个bar空间,但是看定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #define  PCI_NUM_REGIONS 7 struct  PCIDevice  {		     PCIIORegion io_regions[PCI_NUM_REGIONS]; } typedef  struct  PCIIORegion  {    pcibus_t  addr;  #define  PCI_BAR_UNMAPPED (~(pcibus_t)0)     pcibus_t  size;     uint8_t  type;     MemoryRegion *memory;     MemoryRegion *address_space; } PCIIORegion; 
 
实际上region有7个,为什么上面只遍历了6个?因为最后一个region实际上是ROM空间,前六个是RAM空间,在这里我们用不到ROM,因此只遍历六个。具体来看下面的代码,这个代码用于给device添加option rom:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static  void  pci_add_option_rom (PCIDevice *pdev, bool  is_default_rom,                                Error **errp)  {  	   	pci_register_bar(pdev, PCI_ROM_SLOT, 0 , &pdev->rom);  } void  pci_register_bar (PCIDevice *pci_dev, int  region_num,                       uint8_t  type, MemoryRegion *memory)  {  	   	    if  (region_num == PCI_ROM_SLOT) {                   wmask |= PCI_ROM_ADDRESS_ENABLE;     } } 
 
基本可以看出来是用于ROM的。同样的,这几个region的初始化来源也是pci_register_bar()函数,不过该函数只是注册了一下,并没有分配到实际的地址,地址指的是换成上述结构体来说的话就是PCIIORegion->addr。更新地址的函数在pci_update_mapping()。其中io_regions的内容都是从MemoryRegion结构体中迁移过来的。具体怎么注册bar空间的后续会提到一部分。这一块推荐看这篇文章 。
继续看qpci_iomap()函数,这个函数信息量比较大,我拓展开来看了好久才明白这个函数是做什么的。简单概括一下就是以QTest的形式重新分配PCI设备的bar地址。 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 QPCIBar qpci_iomap (QPCIDevice *dev, int  barno, uint64_t  *sizeptr)   {    QPCIBus *bus = dev->bus;     static  const  int  bar_reg_map[] = {         PCI_BASE_ADDRESS_0, PCI_BASE_ADDRESS_1, PCI_BASE_ADDRESS_2,         PCI_BASE_ADDRESS_3, PCI_BASE_ADDRESS_4, PCI_BASE_ADDRESS_5,     };     QPCIBar bar;     int  bar_reg;     uint32_t  addr, size;     uint32_t  io_type;     uint64_t  loc;     g_assert(barno >= 0  && barno <= 5 );     bar_reg = bar_reg_map[barno];     qpci_config_writel(dev, bar_reg, 0xFFFFFFFF );      addr = qpci_config_readl(dev, bar_reg);      io_type = addr & PCI_BASE_ADDRESS_SPACE;      if  (io_type == PCI_BASE_ADDRESS_SPACE_IO) {         addr &= PCI_BASE_ADDRESS_IO_MASK;     } else  {         addr &= PCI_BASE_ADDRESS_MEM_MASK;     }     g_assert(addr);      size = 1U  << ctz32(addr);      if  (sizeptr) {         *sizeptr = size;     } 		 } 
 
这里有两个比较疑惑的点,在标记1处,先写bar地址为0xFFFFFFFF,后读bar地址,不过当我调试的时候,得到的addr并不是0xFFFFFFFF。第二点在标记2处,ctz32()是取末尾零的个数,为什么这样就能得到bar空间的size?
这两个点可以结合起来一起看。前面我们提到注册io_regions的函数为pci_register_bar()。这里我们仍然以virtio-vga设备为栗子。在virtio-vga.c初始化设备中存在注册函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 static  void  virtio_vga_base_realize (VirtIOPCIProxy *vpci_dev, Error **errp)  {  	   	pci_register_bar(&vpci_dev->pci_dev, 0 ,                      PCI_BASE_ADDRESS_MEM_PREFETCH, &vga->vram);  } void  pci_register_bar (PCIDevice *pci_dev, int  region_num,                       uint8_t  type, MemoryRegion *memory)  {    PCIIORegion *r;     uint32_t  addr;      uint64_t  wmask;     pcibus_t  size = memory_region_size(memory);     uint8_t  hdr_type;     assert(region_num >= 0 );     assert(region_num < PCI_NUM_REGIONS);     assert(is_power_of_2(size));          hdr_type =              pci_dev->config[PCI_HEADER_TYPE] & ~PCI_HEADER_TYPE_MULTI_FUNCTION;     assert(hdr_type != PCI_HEADER_TYPE_BRIDGE || region_num < 2 );     r = &pci_dev->io_regions[region_num];      r->addr = PCI_BAR_UNMAPPED;      r->size = size;      r->type = type;      r->memory = memory;      r->address_space = type & PCI_BASE_ADDRESS_SPACE_IO                         ? pci_get_bus(pci_dev)->address_space_io                         : pci_get_bus(pci_dev)->address_space_mem;     wmask = ~(size - 1 );      if  (region_num == PCI_ROM_SLOT) {                  wmask |= PCI_ROM_ADDRESS_ENABLE;     }     addr = pci_bar(pci_dev, region_num);      pci_set_long(pci_dev->config + addr, type);      if  (!(r->type & PCI_BASE_ADDRESS_SPACE_IO) &&         r->type & PCI_BASE_ADDRESS_MEM_TYPE_64) {         pci_set_quad(pci_dev->wmask + addr, wmask);         pci_set_quad(pci_dev->cmask + addr, ~0U LL);     } else  {         pci_set_long(pci_dev->wmask + addr, wmask & 0xffffffff );          pci_set_long(pci_dev->cmask + addr, 0xffffffff );     } } 
 
在调试过程当中,上述vga设备注册的MemoryRegion->size为0x800000,再看标记3处,wmask的结果为0xff800000,PCIDevice->wmask的定义是用于实现R/W字节,应该是用于标记size的作用。最终配置空间的内存情况是这样的:
1 2 3 4 5 6 7 8 pwndbg> x/10 xg 0x62100002bd00  0x62100002bd00 : 0x0010000010501af4       0x0000000003000001 0x62100002bd10 : 0x0000000000000008       0x000000000000000c 0x62100002bd20 : 0x0000000000000000       0x11001af400000000 0x62100002bd30 : 0x0000009800000000       0x0000010000000000 0x62100002bd40 : 0x0000000201100009       0x0000080000001000    
 
回到qpci_iomap()函数,当调用标记1处的函数qpci_config_writel()时,最终会调用hw/pci/pci.c:pci_default_write_config()函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void  pci_default_write_config (PCIDevice *d, uint32_t  addr, uint32_t  val_in, int  l)  {    uint32_t  val = val_in;     for  (i = 0 ; i < l; val >>= 8 , ++i) {         uint8_t  wmask = d->wmask[addr + i];               uint8_t  w1cmask = d->w1cmask[addr + i];         assert(!(wmask & w1cmask));         d->config[addr + i] = (d->config[addr + i] & ~wmask) | (val & wmask);          d->config[addr + i] &= ~(val & w1cmask);      } 		 } 
 
关键读写的操作在标记4处。简单来说就是设置bar空间的地址。根据前面我们知道wmask的结果是0xff800000,那么最终设置的bar地址就是0xff800000,内存布局如下:
1 2 3 4 5 6 7 8 pwndbg> x/10 xg 0x62100002bd00  0x62100002bd00 : 0x0010000010501af4       0x0000000003000001 0x62100002bd10 : 0x00000000ff800008       0x000000000000000c 0x62100002bd20 : 0x0000000000000000       0x11001af400000000 0x62100002bd30 : 0x0000009800000000       0x0000010000000000 0x62100002bd40 : 0x0000000201100009       0x0000080000001000 
 
这也就是为什么前面我们在qpci_iomap()函数标记1先写后读bar地址为什么会出现不是0xffffffff的情况。最终得到的地址是0xff800008。
第一个疑惑点解决了,再来看第二个疑惑点,为什么1U << ctz32(addr);就能获得size。当去掉addr地址的type执行到标记2处的时候,addr的值是0xff800000,ctz32(addr)得到的就是23,1<<23就是size,即0x800000,和最开始注册时的size是一样的。
根据定义,memory space bar和I/O space bar的地址分别是16byte和4byte对齐的:
这就是为什么能按上述来获取size的原因,以及为什么能够利用wmask来做辅助。还不清楚可以自己动手算一下….
qpci_iomap()前半部分算是分析完了,再来看剩余的部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 QPCIBar qpci_iomap (QPCIDevice *dev, int  barno, uint64_t  *sizeptr)   {		     } else  {         loc = QEMU_ALIGN_UP(bus->mmio_alloc_ptr, size);             bus->mmio_alloc_ptr = loc + size;          qpci_config_writel(dev, bar_reg, loc);      }     bar.addr = loc;     return  bar; } 
 
就举例mmio的情况,pio其实一样。最开始的bus->mmio_allco_ptr为0xE0000000:
1 2 3 4 5 6 7 void  qpci_init_pc (QPCIBusPC *qpci, QTestState *qts, QGuestAllocator *alloc)  {  	     qpci->bus.pio_alloc_ptr = 0xc000 ;            qpci->bus.mmio_alloc_ptr = 0xE0000000 ;       qpci->bus.mmio_limit = 0x100000000 ULL;   } 
 
后半部分其实就是从地址0xE0000000开始往后的空间依次分配给PCI的bar空间。 qpci_iomap()函数到这里分析就结束了。
再次回到pci_enum()函数中来,最后剩下的qpci_device_enable()函数就是写PCI设备的配置空间COMMAND内容处,使得PCI设备能够正式启用。
至此,正式fuzz前的准备工作函数generic_fuzz()都已经分析完毕,所有细节部分我们都已经了解过了。剩下的就只有正式fuzz函数以及变异策略函数了。
正式fuzz函数我就不细说,主要就是取libfuzzer的随机输入数据做拆分并设置几个opcode做选择。举个栗子,现在有这么一串随机输入数据:
00 01 02 FF 03 04 05 06 FF 01 FF
 
我设置了几个opcode函数:
1 2 3 4 5 OP_IN, OP_OUT, OP_READ, OP_WRITE ... 
 
以0xFF做分割符,分别取出来作为data和data_len,那么这一串数据就分别对应一下函数:
1 2 3 * 00  01  02     -> op00 (0102 )   -> in (0102 , 2 ) * 03  04  05  06  -> op03 (040506 ) -> write (040506 , 3 ) * 01           -> op01 (-,0 )    -> out (-,0 ) 
 
这就是主fuzz函数的核心思想。
这里我们再看一下获取mmio和pio地址的关键函数get_io_address():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 struct  get_io_cb_info  {    int  index;     int  found;     address_range result; }; typedef  struct  {    ram_addr_t  addr;     ram_addr_t  size;  } address_range; static  bool  get_io_address (address_range *result, AddressSpace *as,                             uint8_t  index,                             uint32_t  offset)   {   																						      FlatView *view;     view = as->current_map;      g_assert(view);     struct  get_io_cb_info  cb_info  = { };     cb_info.index = index;     do  {         flatview_for_each_range(view, get_io_address_cb , &cb_info);     } while  (cb_info.index != index && !cb_info.found);     *result = cb_info.result;     if  (result->size) {         offset = offset % result->size;         result->addr += offset;         result->size -= offset;     }     return  cb_info.found; } void  flatview_for_each_range (FlatView *fv, flatview_cb cb , void  *opaque)  {    FlatRange *fr;     assert(fv);     assert(cb);     FOR_EACH_FLAT_RANGE(fr, fv) {           if  (cb(fr->addr.start, fr->addr.size, fr->mr, opaque))             break ;     } } static  int  get_io_address_cb (Int128 start, Int128 size,                           const  MemoryRegion *mr, void  *opaque)   {    struct  get_io_cb_info  *info  = opaque ;     if  (g_hash_table_lookup(fuzzable_memoryregions, mr)) {          if  (info->index == 0 ) {             info->result.addr = (ram_addr_t )start;             info->result.size = (ram_addr_t )size;             info->found = 1 ;             return  1 ;         }         info->index--;      }     return  0 ; } 
 
阅读上面的代码需要理解QEMU的内存管理机制,不明白的推荐看这两篇,QEMU对虚拟机的内存管理 、QEMU内存模型 。
简单说就是随机选取前面保存起来的MemoryRegion中的一块,进行后续的读写操作。 
但是,这里有读者可能就有疑问了,如果要随机选的话,为什么不直接在存储起来的空间中直接挑呢?而是大费周章的去全局遍历,对比,然后再挑选?
因为我们最终目的是要得到MemoryRegion中的地址、以及size。虽然说看了MemoryRegion的结构体后能够发现其本身就有一个addr属性,但是,这个addr并不是真正意义上的内存地址,还是一个相对偏移,即类似于offset。在QEMU内存管理中,FlatRange中有指向所属MemoryRegion的指针,其中也保存着addr和size,这里的addr才是MemoryRegion真正的地址,具体结构体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct  AddrRange  {    Int128 start;      Int128 size;  }; struct  FlatRange  {    MemoryRegion *mr;      hwaddr offset_in_region;      AddrRange addr;      uint8_t  dirty_log_mask;     bool  romd_mode;     bool  readonly; }; 
 
这下就明白为什么要“大费周章”的去遍历所有Region得到addr了吧。具体的细节看完上面两篇文章后就能弄清楚整一个内存管理的原理了。
最后就是变异策略的函数了,这一块作者写的比较简单,自行阅读代码即可。不过我注意到作者还写了fuzz_dma_read_cb()函数,但是并没有应用过,觉得没有用吗?这一块读者感兴趣的话可以自己阅读以下,看看具体作用到底是什么。
源码分析到这里就完全结束了,感谢阅读。有错误欢迎斧正。
0x03 Summary 读到这里,我想聪明的读者应该已经发现,其实generic_fuzz是一个比较dumb的fuzzer,优点就在能够通用fuzz。而且因为受限于QTest,只有部分设备做了QTest化的处理,所以能够测试的目标有限,我猜测这也是为什么作者只写了针对virtio的几个设备写了特定的fuzz,因为官方在QTest中只写了virtio的一部分。如果想要更高效率的fuzz的话,那还是得需要自己做优化的,我这里仅提供几个思路。
在generic_fuzz中做结构化fuzz,也就是争对某个device做相应的结构体输入化,这样可以充分利用该fuzz的fork的优势。缺点是目标单一,不能多设备fuzz。这里也可以删除他本身的几个opcode函数,例如读写config的可以删除,对于我们来说基本没什么用,可以只保留读写mmio/pio区域的函数,这样可以提高fuzz效率。
 
自己编写QTest化的设备代码,并后续针对这个继续写相应的fuzz。也就是承接上面官方只写了一部分的情况。这是个体力活,不过我个人估计产出会比较明显。
 
在generic_fuzz上做优化(不是单一化),例如提高覆盖率等操作,我自认为它本身还有比较大的改进空间,具体怎么改,我还没有可行的思路,知道的读者麻烦交流一下:)。
 
 
0x04 Reference 
https://www.cnblogs.com/ccxikka/p/9477530.html  
https://richardweiyang-2.gitbook.io/kernel-exploring/00-kvm/01-memory_virtualization/01_1-qemu_memory_model  
https://qemu.readthedocs.io/en/latest/devel/qgraph.html#qgraph  
https://blog.csdn.net/weixin_43780260/article/details/104410063